package biz.datalk.industrialland.common.advice;

import biz.datalk.commons.utils.BasicParserUtils;
import biz.datalk.industrialland.common.advice.cfg.DatalkCryptoCfgs;
import biz.datalk.industrialland.common.component.AppEncryptSecurityKeyComponent;
import biz.datalk.industrialland.common.def.CommonConstant;
import biz.datalk.industrialland.common.encrypt.AESTool;
import biz.datalk.industrialland.common.encrypt.RSATool;
import biz.datalk.industrialland.common.encrypt.annotation.RSAES;
import biz.datalk.industrialland.common.encrypt.sm.GMUtils;
import biz.datalk.industrialland.common.json.CustomerJsonSerializer;
import biz.datalk.industrialland.common.json.annotation.Json;
import biz.datalk.industrialland.common.json.annotation.Jsons;
import biz.datalk.industrialland.common.result.UnifyResult;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author tarofang@163.com
 * @date 2019-08-25
 */
@SuppressWarnings({"NullableProblems", "JavaDoc", "WeakerAccess", "UnusedAssignment"})
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@RestControllerAdvice
@ConditionalOnProperty(name = {"datalk.crypto.enable", "datalk.crypto.response-encrypt"}, havingValue = "true")
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    private final static Logger logger = LoggerFactory.getLogger(EncryptResponseBodyAdvice.class);

    private final static ConcurrentHashMap<String, CustomerJsonSerializer> customerJsonSerializerCacheMap = new ConcurrentHashMap<>();

    private final DatalkCryptoCfgs datalkCryptoCfgs;

    @Autowired
    public EncryptResponseBodyAdvice(DatalkCryptoCfgs datalkCryptoCfgs) {
        this.datalkCryptoCfgs = datalkCryptoCfgs;
    }

    @Autowired
    private AppEncryptSecurityKeyComponent appEncryptSecurityKeyComponent;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> converterType) {
        logger.debug("EncryptResponseBodyAdvice.supports into");
        try {
            if (!datalkCryptoCfgs.isEnable()) {
                return false;
            }
            if (!datalkCryptoCfgs.isResponseEncrypt()) {
                return false;
            }

            Method method = methodParameter.getMethod();
            if (method == null) {
                return false;
            }
            boolean methodSupports = method.isAnnotationPresent(RSAES.class);
            logger.debug("EncryptResponseBodyAdvice.supports methodSupports:{} -> {}", method.getName(), methodSupports);
            return methodSupports;
        } finally {
            logger.debug("EncryptResponseBodyAdvice.supports out");
        }
    }


    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        Method method = returnType.getMethod();
        if (method == null) {
            logger.debug("Invalid. Method must not be null.");
            return body;
        }

        if (method.isAnnotationPresent(RSAES.class)) {
            RSAES rsaes = returnType.getMethodAnnotation(RSAES.class);
            if (rsaes != null && rsaes.outEncrypt()) {
                logger.info("@RSAES ----- method:[{}] ------ encrypt", method.getName());
                return encodeAesRsa(returnType, body, request);
            }
        }

        return body;
    }

    /**
     * 混合加密
     *
     * @return
     */
    private Object encodeAesRsa(MethodParameter methodParameter, Object body, ServerHttpRequest request) {
        Method method = methodParameter.getMethod();
        String className = method.getDeclaringClass().getName();
        String methodName = method.getName();
        int parameterCount = method.getParameterCount();
        String httpMethod = request.getMethodValue();
        String requestUri = request.getURI().toString();

        String cacheKey = className + "_" + methodName + "_" + parameterCount + "_" + requestUri;
        CustomerJsonSerializer jsonSerializer = customerJsonSerializerCacheMap.get(cacheKey);
        if (jsonSerializer == null) {
            Annotation[] annos = methodParameter.getMethodAnnotations();
            CustomerJsonSerializer jsonSerializerNew = new CustomerJsonSerializer();
            Arrays.asList(annos).forEach(a -> {
                if (a instanceof Json) {
                    Json json = (Json) a;
                    jsonSerializerNew.filter(json);
                } else if (a instanceof Jsons) {
                    Jsons jsons = (Jsons) a;
                    Arrays.asList(jsons.value()).forEach(jsonSerializerNew::filter);
                }
            });
            jsonSerializer = jsonSerializerNew;
            customerJsonSerializerCacheMap.put(cacheKey, jsonSerializerNew);
        }


        try {
            HttpHeaders headers = request.getHeaders();
            List<String> clientTypeList = headers.getOrEmpty(CommonConstant.X_CLIENT_TYPE);
            List<String> clientVerList = headers.getOrEmpty(CommonConstant.X_CLIENT_VER);
            String clientType = CollectionUtils.isEmpty(clientTypeList) ? StringUtils.EMPTY : clientTypeList.get(0);
            String clientVer = CollectionUtils.isEmpty(clientVerList) ? StringUtils.EMPTY : clientVerList.get(0);
            int clientVerInt = BasicParserUtils.parseInt(clientVer, 0);

            // boolean useGm = datalkCryptoCfgs.useGm(RequestHeaderUtil.getXClientTypeStr(), RequestHeaderUtil.getXClientVer());
            boolean useGm = datalkCryptoCfgs.useGm(clientType, clientVerInt);
            // 生成aes秘钥 或 sm4Iv
            // String aseKey = getRandomString(16);
            String aseKey = useGm ? genRandomHex(32) : getRandomString(16);
            // rsa加密
            // String enkey = RSATool.encrypt(aseKey, datalkCryptoCfgs.getClient().getPublicKey().replaceAll("\\s", ""));
            // add 2022-10-08 by tarofang@163.com S
            // 通过 appkey 获取加解密秘钥
            ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;
            HttpServletRequest httpServletRequest = servletServerHttpRequest.getServletRequest();
            String appkey = (String) httpServletRequest.getAttribute(CommonConstant.HTTP_REQ_ATTR_APPKEY);
            if (logger.isDebugEnabled()) {
                logger.debug("[appkey={}]", appkey);
            }
            AppSecurityInfo appSecurityInfo = appEncryptSecurityKeyComponent.getByAppKey(appkey);
            String clientPublicKey;
            if (appSecurityInfo != null) {
                clientPublicKey = appSecurityInfo.getClientPublicKey();
            } else {
                clientPublicKey = useGm ? datalkCryptoCfgs.getGmClient().getPublicKey() : datalkCryptoCfgs.getClient().getPublicKey();
            }
            if (StringUtils.isBlank(clientPublicKey)) {
                logger.warn("clientPublicKey is blank. [appkey={}, useGm={}]", appkey, useGm);
            }
            // add 2022-10-08 by tarofang@163.com E
            String enkey = useGm //
                    ? GMUtils.sm2Encrypt(clientPublicKey, aseKey)  //
                    // ? GMUtils.sm2Encrypt(datalkCryptoCfgs.getGmClient().getPublicKey(), aseKey)  //
                    : RSATool.encrypt(aseKey, clientPublicKey);
            // aes加密
            String endata = "";

            // 返回值类型
            UnifyResult res;
            if (body instanceof String) {
                res = UnifyResult.success();
                logger.info("response data ===> {}", body);
                // endata = AESTool.encrypt((String) body, aseKey);
                endata = useGm //
                        ? GMUtils.sm4Encrypt((String) body, aseKey) //
                        : AESTool.encrypt((String) body, aseKey);
            } else if (body instanceof UnifyResult) {
                res = (UnifyResult) body;
                // endata = AESTool.encrypt(JsonUtil.getJsonFromObject(res.getData()), aseKey);
                // endata = AESTool.encrypt(JsonUtil.toJsonString(res.getData(), "yyyy-MM-dd HH:mm:ss"), aseKey);
                Object data = res.getData();
                if (data == null) {
                    logger.info("response data is null");
                    return res;
                }
                // add 2021-08-06 by tarofang@163.com S
                // jackson writeValueAsString 入参为字符窜时，出参会在入参的基础上加上双引号
                String preEncryptStr = "";
                if (data instanceof String) {
                    preEncryptStr = (String) data;
                } else {
                    preEncryptStr = jsonSerializer.toJson(res.getData());
                }
                // add 2021-08-06 by tarofang@163.com E
                logger.info("response data ===> {}", preEncryptStr);
                // endata = AESTool.encrypt(preEncryptStr, aseKey);
                endata = useGm //
                        ? GMUtils.sm4Encrypt(preEncryptStr, aseKey) //
                        : AESTool.encrypt(preEncryptStr, aseKey);
            } else {
                res = UnifyResult.success();
                // endata = AESTool.encrypt(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body), aseKey);
                // endata = AESTool.encrypt(JsonUtil.getJsonFromObject(body), aseKey);
                // endata = AESTool.encrypt(JsonUtil.toJsonString(body, "yyyy-MM-dd HH:mm:ss"), aseKey);
                logger.info("response data ===> {}", jsonSerializer.toJson(body));
                // endata = AESTool.encrypt(jsonSerializer.toJson(body), aseKey);
                endata = useGm //
                        ? GMUtils.sm4Encrypt(jsonSerializer.toJson(body), aseKey) //
                        : AESTool.encrypt(jsonSerializer.toJson(body), aseKey);
            }

            return UnifyResult.create(res.getRsCode(), res.getRsCause()).mapData().put("endata", endata).put("enkey", enkey);
        } catch (Exception ex) {
            // Method method = methodParameter.getMethod();
            // String methodName = method != null ? method.getName() : "unknow method";
            logger.error("method [{}] RSA+AES encrypt exception:[{}] ：", methodName, ex.getMessage());
        }
        return body;
    }


    /**
     * 创建指定位数的随机字符串
     *
     * @param length 表示生成字符串的长度
     *
     * @return 字符串
     */
    public static String getRandomString(int length) {
        return RandomStringUtils.randomAlphanumeric(length);
        // String base = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        // Random random = new Random();
        // StringBuffer sb = new StringBuffer();
        // int baseLength = base.length();
        // for (int i = 0; i < length; i++) {
        //     int number = random.nextInt(baseLength);
        //     sb.append(base.charAt(number));
        // }
        // return sb.toString();
    }

    public static String genRandomHex(int len) {
        return RandomStringUtils.random(len, "0123456789abcdef");
    }

    public static void main(String[] args) {
        System.out.println(getRandomString(16));
    }

}
